package xin.petstore.framework.orm.mybatis.paginator;


import xin.petstore.framework.orm.mybatis.paginator.dialect.Dialect;
import xin.petstore.framework.orm.mybatis.paginator.domain.PageBounds;
import xin.petstore.framework.orm.mybatis.paginator.domain.PageList;
import xin.petstore.framework.orm.mybatis.paginator.domain.Paginator;
import xin.petstore.framework.orm.mybatis.paginator.support.PropertiesHelper;
import xin.petstore.framework.orm.mybatis.paginator.support.SQLHelp;

import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Intercepts({@org.apache.ibatis.plugin.Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
        Object.class, RowBounds.class, org.apache.ibatis.session.ResultHandler.class})})
public class OffsetLimitInterceptor
        implements Interceptor {
    private static Logger logger = LoggerFactory.getLogger(OffsetLimitInterceptor.class);
    static int MAPPED_STATEMENT_INDEX = 0;
    static int PARAMETER_INDEX = 1;
    static int ROWBOUNDS_INDEX = 2;
    static int RESULT_HANDLER_INDEX = 3;

    static ExecutorService Pool = Executors.newCachedThreadPool();
    String dialectClass;
    boolean asyncTotalCount = false;

    public Object intercept(Invocation invocation) throws Throwable {
        Executor executor = (Executor) invocation.getTarget();
        Object[] queryArgs = invocation.getArgs();
        MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
        Object parameter = queryArgs[PARAMETER_INDEX];
        RowBounds rowBounds = (RowBounds) queryArgs[ROWBOUNDS_INDEX];
        PageBounds pageBounds = new PageBounds(rowBounds);

        if ((pageBounds.getOffset() == 0) &&
                (pageBounds.getLimit() == 2147483647) &&
                (pageBounds.getOrders().isEmpty())) {
            return invocation.proceed();
        }

        try {
            Class clazz = Class.forName(this.dialectClass);
            Constructor constructor = clazz.getConstructor(new Class[]{MappedStatement.class, Object.class, PageBounds.class});
            dialect = (Dialect) constructor.newInstance(new Object[]{ms, parameter, pageBounds});
        } catch (Exception e) {
            Dialect dialect;
            throw new ClassNotFoundException("Cannot create dialect instance: " + this.dialectClass, e);
        }
        Dialect dialect;
        BoundSql boundSql = ms.getBoundSql(parameter);

        queryArgs[MAPPED_STATEMENT_INDEX] = copyFromNewSql(ms, boundSql, dialect.getPageSQL(), dialect.getParameterMappings(), dialect.getParameterObject());
        queryArgs[PARAMETER_INDEX] = dialect.getParameterObject();
        queryArgs[ROWBOUNDS_INDEX] = new RowBounds(0, 2147483647);

        Boolean async = Boolean.valueOf(pageBounds.getAsyncTotalCount() == null ? this.asyncTotalCount : pageBounds.getAsyncTotalCount().booleanValue());
        Future listFuture = call(new Callable(invocation) {
                                     public List call() throws Exception {
                                         return (List) this.val$invocation.proceed();
                                     }
                                 }
                , async.booleanValue());

        if (pageBounds.isContainsTotalCount()) {
            Callable countTask = new Callable(ms, executor, parameter, boundSql, dialect, pageBounds) {
                public Object call() throws Exception {
                    Cache cache = this.val$ms.getCache();
                    Integer count;
                    if ((cache != null) && (this.val$ms.isUseCache()) && (this.val$ms.getConfiguration().isCacheEnabled())) {
                        CacheKey cacheKey = this.val$executor.createCacheKey(this.val$ms, this.val$parameter, new PageBounds(), OffsetLimitInterceptor.this.copyFromBoundSql(this.val$ms, this.val$boundSql, this.val$dialect.getCountSQL(), this.val$boundSql.getParameterMappings(), this.val$boundSql.getParameterObject()));
                        Integer count = (Integer) cache.getObject(cacheKey);
                        if (count == null) {
                            count = Integer.valueOf(SQLHelp.getCount(this.val$ms, this.val$executor.getTransaction(), this.val$parameter, this.val$boundSql, this.val$dialect));
                            cache.putObject(cacheKey, count);
                        }
                    } else {
                        count = Integer.valueOf(SQLHelp.getCount(this.val$ms, this.val$executor.getTransaction(), this.val$parameter, this.val$boundSql, this.val$dialect));
                    }
                    return new Paginator(this.val$pageBounds.getPage(), this.val$pageBounds.getLimit(), count.intValue());
                }
            };
            Future countFutrue = call(countTask, async.booleanValue());
            return new PageList((Collection) listFuture.get(), (Paginator) countFutrue.get());
        }

        return listFuture.get();
    }

    private <T> Future<T> call(Callable callable, boolean async) {
        if (async) {
            return Pool.submit(callable);
        }
        FutureTask future = new FutureTask(callable);
        future.run();
        return future;
    }

    private MappedStatement copyFromNewSql(MappedStatement ms, BoundSql boundSql, String sql, List<ParameterMapping> parameterMappings, Object parameter) {
        BoundSql newBoundSql = copyFromBoundSql(ms, boundSql, sql, parameterMappings, parameter);
        return copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
    }

    private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql, List<ParameterMapping> parameterMappings, Object parameter) {
        BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, parameterMappings, parameter);
        for (ParameterMapping mapping : boundSql.getParameterMappings()) {
            String prop = mapping.getProperty();
            if (boundSql.hasAdditionalParameter(prop)) {
                newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
            }
        }
        return newBoundSql;
    }

    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());

        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if ((ms.getKeyProperties() != null) && (ms.getKeyProperties().length != 0)) {
            StringBuffer keyProperties = new StringBuffer();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }

        builder.timeout(ms.getTimeout());

        builder.parameterMap(ms.getParameterMap());

        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());

        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }

    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    public void setProperties(Properties properties) {
        PropertiesHelper propertiesHelper = new PropertiesHelper(properties);
        String dialectClass = propertiesHelper.getRequiredString("dialectClass");
        if (StringUtils.isNotEmpty(dialectClass)) {
            setDialectClass(dialectClass);
        }
        setAsyncTotalCount(propertiesHelper.getBoolean("asyncTotalCount", false));

        setPoolMaxSize(propertiesHelper.getInt("poolMaxSize", 0));
    }

    public void setDialectClass(String dialectClass) {
        logger.debug("dialectClass: {} ", dialectClass);
        this.dialectClass = dialectClass;
    }

    public void setAsyncTotalCount(boolean asyncTotalCount) {
        logger.debug("asyncTotalCount: {} ", Boolean.valueOf(asyncTotalCount));
        this.asyncTotalCount = asyncTotalCount;
    }

    public void setPoolMaxSize(int poolMaxSize) {
        if (poolMaxSize > 0) {
            logger.debug("poolMaxSize: {} ", Integer.valueOf(poolMaxSize));
            Pool = Executors.newFixedThreadPool(poolMaxSize);
        } else {
            Pool = Executors.newCachedThreadPool();
        }
    }

    public static class BoundSqlSqlSource
            implements SqlSource {
        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        public BoundSql getBoundSql(Object parameterObject) {
            return this.boundSql;
        }
    }
}